/*
 * Copyright (C) 2012 Free Software Foundation, Inc.
 *
 * Author: Nikos Mavrogiannopoulos
 *
 * This file is part of GnuTLS.
 *
 * The GnuTLS is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <gnutls_errors.h>
#include <gnutls_int.h>
#include <gnutls/crypto.h>
#include <gnutls_errors.h>
#include <accelerated/cryptodev.h>

#ifdef ENABLE_CRYPTODEV

#include <fcntl.h>
#include <sys/ioctl.h>
#include <crypto/cryptodev.h>

#ifndef CRYPTO_CIPHER_MAX_KEY_LEN
#define CRYPTO_CIPHER_MAX_KEY_LEN 64
#endif

#ifndef EALG_MAX_BLOCK_LEN
#define EALG_MAX_BLOCK_LEN 16
#endif


#ifdef CIOCAUTHCRYPT

#define GCM_BLOCK_SIZE 16

struct cryptodev_gcm_ctx
{
  struct session_op sess;
  struct crypt_auth_op cryp;
  uint8_t iv[GCM_BLOCK_SIZE];
  uint8_t tag[GCM_BLOCK_SIZE];
  
  void* auth_data;
  unsigned int auth_data_size;
  
  int op; /* whether encryption op has been executed */

  int cfd;
};

static void
aes_gcm_deinit (void *_ctx)
{
  struct cryptodev_gcm_ctx *ctx = _ctx;

  ioctl (ctx->cfd, CIOCFSESSION, &ctx->sess.ses);
  gnutls_free (ctx);
}

static const int cipher_map[] = {
  [GNUTLS_CIPHER_AES_128_GCM] = CRYPTO_AES_GCM,
  [GNUTLS_CIPHER_AES_256_GCM] = CRYPTO_AES_GCM,
};

static int
aes_gcm_cipher_init (gnutls_cipher_algorithm_t algorithm, void **_ctx, int enc)
{
  struct cryptodev_gcm_ctx *ctx;

  *_ctx = gnutls_calloc (1, sizeof (struct cryptodev_gcm_ctx));
  if (*_ctx == NULL)
    {
      gnutls_assert ();
      return GNUTLS_E_MEMORY_ERROR;
    }


  ctx = *_ctx;

  ctx->cfd = _gnutls_cryptodev_fd;
  ctx->sess.cipher = cipher_map[algorithm];
  ctx->cryp.iv = ctx->iv;

  return 0;
}

static int
aes_gcm_cipher_setkey (void *_ctx, const void *userkey, size_t keysize)
{
  struct cryptodev_gcm_ctx *ctx = _ctx;

  ctx->sess.keylen = keysize;
  ctx->sess.key = (void*)userkey;

  if (ioctl (ctx->cfd, CIOCGSESSION, &ctx->sess))
    {
      gnutls_assert ();
      return GNUTLS_E_CRYPTODEV_IOCTL_ERROR;
    }
  ctx->cryp.ses = ctx->sess.ses;

  return 0;
}

static int
aes_gcm_setiv (void *_ctx, const void *iv, size_t iv_size)
{
  struct cryptodev_gcm_ctx *ctx = _ctx;

  if (iv_size != GCM_BLOCK_SIZE - 4)
    return GNUTLS_E_INVALID_REQUEST;

  memcpy (ctx->iv, iv, GCM_BLOCK_SIZE - 4);

  ctx->cryp.iv = (void*)ctx->iv;

  return 0;
}

static int
aes_gcm_encrypt (void *_ctx, const void *src, size_t src_size,
                 void *dst, size_t dst_size)
{
  struct cryptodev_gcm_ctx *ctx = _ctx;

  /* the GCM in kernel will place the tag after the
   * encrypted data.
   */
  if (dst_size < src_size + GCM_BLOCK_SIZE)
    return gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);

  ctx->cryp.len = src_size;
  ctx->cryp.src = (void *) src;
  ctx->cryp.dst = dst;
  ctx->cryp.op = COP_ENCRYPT;

  ctx->cryp.auth_len = ctx->auth_data_size;
  ctx->cryp.auth_src = ctx->auth_data;

  if (ioctl (ctx->cfd, CIOCAUTHCRYPT, &ctx->cryp))
    {
      gnutls_assert ();
      return GNUTLS_E_CRYPTODEV_IOCTL_ERROR;
    }
  
  ctx->cryp.auth_len = 0;
  ctx->op = 1;
  memcpy(ctx->tag, &((uint8_t*)dst)[src_size], GCM_BLOCK_SIZE);
  return 0;
}

static int
aes_gcm_decrypt (void *_ctx, const void *src, size_t src_size,
                 void *dst, size_t dst_size)
{
  struct cryptodev_gcm_ctx *ctx = _ctx;

  /* the GCM in kernel will place the tag after the
   * encrypted data.
   */
  ctx->cryp.len = src_size + GCM_BLOCK_SIZE;
  ctx->cryp.src = (void *) src;
  ctx->cryp.dst = dst;
  ctx->cryp.op = COP_DECRYPT;

  ctx->cryp.auth_len = ctx->auth_data_size;
  ctx->cryp.auth_src = ctx->auth_data;

  if (ioctl (ctx->cfd, CIOCAUTHCRYPT, &ctx->cryp))
    {
      gnutls_assert ();
      return GNUTLS_E_CRYPTODEV_IOCTL_ERROR;
    }

  ctx->cryp.auth_len = 0;
  ctx->op = 1;
  memcpy(ctx->tag, &((uint8_t*)dst)[src_size], GCM_BLOCK_SIZE);
  return 0;
}

static int
aes_gcm_auth (void *_ctx, const void *src, size_t src_size)
{
  struct cryptodev_gcm_ctx *ctx = _ctx;

  ctx->op = 0;
  ctx->auth_data = (void*)src;
  ctx->auth_data_size = src_size;

  return 0;
}

static void
aes_gcm_tag (void *_ctx, void *tag, size_t tagsize)
{
  struct cryptodev_gcm_ctx *ctx = _ctx;
  
  if (ctx->op == 0)
    {
      ctx->cryp.len = 0;
      ctx->cryp.src = NULL;
      ctx->cryp.dst = ctx->tag;
      ctx->cryp.op = COP_ENCRYPT;

      ctx->cryp.auth_len = ctx->auth_data_size;
      ctx->cryp.auth_src = ctx->auth_data;

      if (ioctl (ctx->cfd, CIOCAUTHCRYPT, &ctx->cryp))
        {
          gnutls_assert ();
          return;
        }
    }

  memcpy(tag, ctx->tag, tagsize);
  ctx->op = 0;
}

static const gnutls_crypto_cipher_st cipher_struct = {
  .init = aes_gcm_cipher_init,
  .setkey = aes_gcm_cipher_setkey,
  .setiv = aes_gcm_setiv,
  .encrypt = aes_gcm_encrypt,
  .decrypt = aes_gcm_decrypt,
  .deinit = aes_gcm_deinit,
  .tag = aes_gcm_tag,
  .auth = aes_gcm_auth,
};

int
_cryptodev_register_gcm_crypto (int cfd)
{
  struct session_op sess;
  uint8_t fake_key[CRYPTO_CIPHER_MAX_KEY_LEN];
  unsigned int i;
  int ret;
#ifdef CIOCGSESSINFO
  struct session_info_op siop;

  memset(&siop, 0, sizeof(siop));
#endif

  memset (&sess, 0, sizeof (sess));

  for (i = 0; i < sizeof (cipher_map) / sizeof (cipher_map[0]);
       i++)
    {
      if (cipher_map[i] == 0)
        continue;

      /* test if a cipher is support it and if yes register it */
      sess.cipher = cipher_map[i];
      sess.keylen = gnutls_cipher_get_key_size (i);
      sess.key = fake_key;

      if (ioctl (cfd, CIOCGSESSION, &sess))
        {
          continue;
        }

#ifdef CIOCGSESSINFO
      siop.ses = sess.ses; /* do not register ciphers that are not hw accelerated */
      if (ioctl(cfd, CIOCGSESSINFO, &siop) == 0) 
        {
          if (!(siop.flags & SIOP_FLAG_KERNEL_DRIVER_ONLY))
            {
              ioctl (cfd, CIOCFSESSION, &sess.ses);
              continue;
            }
        }
#endif

      ioctl (cfd, CIOCFSESSION, &sess.ses);

      _gnutls_debug_log ("/dev/crypto: registering: %s\n",
                         gnutls_cipher_get_name (i));
      ret = gnutls_crypto_single_cipher_register (i, 90, &cipher_struct);
      if (ret < 0)
        {
          gnutls_assert ();
          return ret;
        }

    }

  return 0;
}

#endif /* CIOCAUTHCRYPT */

#endif /* ENABLE_CRYPTODEV */
